SpringBoot Quartz 定时任务

SpringBoot Quartz 定时任务

简介

Quartz是一个完全由Java编写的开源作业调度框架。项目由于需要使用定时任务,考虑到其与Spring里面的SpringTask在cron表达式的一个区别,所以选择了Quartz(具体什么区别末尾会提及)。不得不说,Quartz在定时任务这块操作起来简易得让人发指。起一个定时任务超级简单。

  • 创建实现org.quartz.Job接口的java类,其中只有一个方法:execute (JobExecutionContext context) throws JobExecutionException,将要实现的任务逻辑添加进execute这个方法里
  • 创建JobDetail
  • 创建触发器(CronTrigger)
  • Scheduler开启定时任务

名词阐述

Job

是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;

JobDetail

Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。

Trigger

是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;

Scheduler

代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。

项目需求

近期做的一个项目中,有一个这样的需求,活动报名通道需要根据预设好的截止时间结束活动报名,截止时间可以为空,时间格式是yyyy-MM-dd HH:mm:ss

配置

加入Quartz,需要以下两个依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>

应用场景

在将quartz应用到项目之前,还了解到quartz可应用于单任务以及多任务

单任务

参考别人的文章

虽然单任务不是项目需求所要求的解决方案,还是写一下,以后需要的时候可以翻出来看看,而单任务在我解决项目需求前,曾动手实验了一番,正是因为跑过代码,才发现单任务不太适合这个项目需求

单任务即整个项目就只有一个定时任务,与用户数量无关,比如定时清理数据库日志,定时全站通告等等

任务

1
2
3
4
5
6
7
@Configuration
@EnableScheduling
public class ScheduleTask {
public void scheduleTest() {
System.out.println("scheduleTest");
}
}

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Configuration
public class QuartzConfigration {

//设置任务
@Bean(name = "jobDetail")
public MethodInvokingJobDetailFactoryBean detailFactoryBean(ScheduleTask task) {
// ScheduleTask为需要执行的任务
MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
/*
* 是否并发执行
* 例如每5s执行一次任务,但是当前任务还没有执行完,就已经过了5s了,
* 如果此处为true,则下一个任务会bing执行,如果此处为false,则下一个任务会等待上一个任务执行完后,再开始执行
*/
jobDetail.setConcurrent(true);

jobDetail.setName("scheduler");// 设置任务的名字(必须)
jobDetail.setGroup("scheduler_group");// 设置任务的分组,这些属性都可以存储在数据库中,在多任务的时候使用(必须)

/*
* 这两行代码表示执行task对象中的scheduleTest方法。定时执行的逻辑都在scheduleTest。
*/
jobDetail.setTargetObject(task);

jobDetail.setTargetMethod("scheduleTest"); //用字符串指明了要执行的方法
return jobDetail;
}

//设置触发器
@Bean(name = "jobTrigger")
public CronTriggerFactoryBean cronJobTrigger(MethodInvokingJobDetailFactoryBean jobDetail) {
CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
tigger.setJobDetail(jobDetail.getObject());
tigger.setCronExpression("0/6 * * * * ?");// 表示每隔6秒钟执行一次,cron表达式
tigger.setName("myTigger");// trigger的name
return tigger;

}

//设置scheduler,触发任务
@Bean(name = "scheduler")
public SchedulerFactoryBean schedulerFactory(Trigger cronJobTrigger) {
SchedulerFactoryBean bean = new SchedulerFactoryBean();
//设置是否任意一个已定义的Job会覆盖现在的Job。默认为false,即已定义的Job不会覆盖现有的Job。
bean.setOverwriteExistingJobs(true);
// 延时启动,应用启动5秒后 ,定时器才开始启动
bean.setStartupDelay(5);
// 注册定时触发器
bean.setTriggers(cronJobTrigger);
return bean;
}
//多任务时的Scheduler,动态设置Trigger。一个SchedulerFactoryBean可能会有多个Trigger
@Bean(name = "multitaskScheduler")
public SchedulerFactoryBean schedulerFactoryBean(){
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
return schedulerFactoryBean;
}
}

上述设置时,会在项目启动的5秒之后,每隔6秒执行一次啊ScheduleTask中的scheduleTest方法。若想动态改变cron表达式,可以重新生成一个trigger,schedule重新规划这个触发器

动态改变cron表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ResponseBody
@GetMapping("/{prod}/quart")
public String quartzTest(@PathVariable Integer prod) throws SchedulerException{
//获取这个触发器
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(cronTrigger.getKey());
//触发器中的cron表达式
String currentCron = trigger.getCronExpression();// 当前Trigger使用的
System.err.println("当前trigger使用的-"+currentCron);

//改变cron表达式,1秒钟执行一次
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?");

// 按新的cronExpression表达式重新构建trigger
trigger = (CronTrigger) scheduler.getTrigger(cronTrigger.getKey());
trigger = trigger.getTriggerBuilder().withIdentity(cronTrigger.getKey())
.withSchedule(scheduleBuilder).build();

// 按新的trigger重新设置job执行
scheduler.rescheduleJob(cronTrigger.getKey(), trigger);
return prod+"-这是quartz测试";
}

多任务

上述的单任务在项目启动时就会启动,但是在项目往往的场景是项目启动时,不要你启动,而只需要用户指定启动的时候,启动某一特定的任务。因此这一动态多任务相比单任务要简单得多

任务

实现了Job接口的任务

实际情况是,有多少个任务就有多少个类实现Job这个接口

上面图片,实现了通过传一个id过来,去数据查找,然后更新。我的处理使将id作为Job的name,然后传过来,再将其转换为Integer类型。若遇到更多的参数的话,应通过使用JobDetail的JobDataMap传输过来,例如

1
2
JobDetail job = JobBuilder.newJob(。。。).withIdentity(..., ...).build();
job.getJobDataMap().putAsString("key", "value");

这种方式传入参数是Quartz比较推荐的。然后再execute方法里再通过JobDataMap取出来,例如

1
2
JobDataMap map = context.getJobDetail().getJobDataMap()
map.getString("key");

配置

开启任务配置

上图步骤如下:

  • 先使用JobBuilder将任务放进去,同时设置name,group,得到一个JobDetail
  • 然后通过CronScheduleBuilder设置触发日期
  • 接着使用TriggerBuilder获取一个触发器
  • Scheduler去规划这个Job和这个触发器什么时候触发

基本步骤如上,当然项目还使用了其他的方法,如下:

其他方法

其他方法

综合上图可以看到,要找到一个任务,只需传入name和group,得到一个JobKey,再通过JobKey获取这个JobDetail。Quartz就是这么简单操作

Cron表达式

既然提及到定时任务,就不得不提一下Cron表达式,详情请参考这篇文章,参考一下大神的博客,受益匪浅

Cron表达式是一个字符串,以5或者6个空格隔开,分成6或者7个域,每一个域代表一个含义

结构

Cron从左到右:秒 分 时 日 月 星期 年份

各字段含义

  • 秒:0~59的整数,允许,- * /四个字符
  • 分:0~59的整数,允许,- * /四个字符
  • 时:0~23的整数,允许, - * /四个字符
  • 日:1~31的整数,允许,- * / L W C ?八个字符
  • 月:1~12的整数,或者JAN-DEC,允许, - * /四个字符
  • 星期:1~7的整数,或者SUN-SAT,允许,- * / L C # ? 八个字符
  • 年:1970~2099,允许,- * /四个字符

注意:
*:表示匹配任意值
?:只能用在日和星期这两个地方,
-:表示范围,如分钟可用5-20 表示5到20分钟
/:表示起始时间开始触发,然后每隔固定时间触发一次,如5/20:可表示5分钟触发一次,而下一次触发为25分钟,再下一次为45分钟
,:表示列出枚举值
L:表示最后,只能出现在日和星期这两个地方,例如5L表示最后一个周四触发
W:表示有效工作日(周一到周五),只能出现在日,系统将在离指定日期的最近有效工作日触发,比如5W,如果5号是周六,将在最近的周五触发,如果5号是在周日,将在最近的周一触发。

#:表示用于确定每个月第几个星期几,比如:4#2 表示某月的第2个星期三

Quartz和SpringTask区别

  1. 第一个也是最重要的一个,就是精确度:

Quarz的精确度可以精确到某年某月某日某时某分某秒,而SpringTask或者别的Timer不行,看了下SpringTask源码,只能够让我们精确到某月某日某时某分某秒以及星期,相比于Quartz差了很多,这也是我一开始尝试SpringTask后,遇到这个坑,再转去Quartz的。

  1. 对异常的处理:

Quartz在某次执行任务过程中抛出异常,不影响下一次任务执行。

SpringTask一旦某个任务执行过程出现异常,整个任务周期结束,不再执行

  1. 任务执行过程

Quartz采用多线程,而Task默认采用单线程串行执行,若任务时间长,后续任务无法展开。

  1. 部署

这个区别还没遇到过,Quartz采用集群方式,分布式部署到多台机器,分配执行定时任务。

总结

在项目中,用上了Quartz以及用之前踩过的坑,能够深入理解到定时任务调度这块还是Quartz做的比较好。在理解过程中,应该寻着源码去探究。如果一个异常跑出来了,这就是学习机会,这可以通过打印出来的堆栈信息一步一步去看里面设置了什么东西。尽量避免面向百度编程吧233333333333

-------------本文结束感谢您的阅读-------------